package cfig.launcher

import cfig.helper.AndroidHelper
import cfig.helper.Helper
import cfig.helper.ZipHelper
import cfig.helper.ZipHelper.Companion.dumpEntry
import cfig.helper.ZipHelper.Companion.pack
import cfig.helper.ZipHelper.Companion.zipEdit
import cfig.ota.*
import cfig.ota.BrilloPropString.Companion.computeAllPropertyStrings
import cfig.ota.BrilloPropString.Companion.finalizeAllPropertyFiles
import cfig.ota.BrilloPropString.Companion.rmMetaData
import cfig.ota.BrilloPropString.Companion.toBrilloString
import cfig.ota.Common.Companion.PARTITIONS_WITH_BUILD_PROP
import cfig.ota.Common.Companion.getBuildProp
import com.fasterxml.jackson.databind.ObjectMapper
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
import org.apache.commons.compress.archivers.zip.ZipFile
import org.apache.commons.compress.archivers.zip.ZipMethod
import org.slf4j.LoggerFactory
import java.io.File
import java.time.Instant
import java.time.ZoneId
import java.util.*
import kotlin.system.exitProcess

@OptIn(ExperimentalUnsignedTypes::class)
class Generator(inTargetFileName: String, val options: OtaOptions) {
    private val theTargetFileName = inTargetFileName
    private val workDir = "build/staging_ota"

    companion object {
        private val log = LoggerFactory.getLogger(Generator::class.java)

        @OptIn(ExperimentalUnsignedTypes::class)
        fun main(args: Array<String>) {
            if (args.size != 1) {
                println("Usage: from <fileName>")
                return
            }
            if (!File(args[0]).exists()) {
                println("\nERROR: File ${args[0]} doesn't exist")
                return
            }

            val opt = File("options.json").let { optionFile ->
                if (optionFile.exists()) {
                    log.info("Loading OtaOptions from options.json ...")
                    ObjectMapper().readValue(optionFile, OtaOptions::class.java)
                } else {
                    log.info("No external OtaOptions, using default ...")
                    ObjectMapper().writerWithDefaultPrettyPrinter().writeValue(File("options.json"), OtaOptions())
                    OtaOptions()
                }
            }

            Generator(args[0], opt).writeABOTAPackageWithBrilloScript()
        }
    }

    private fun addCompatibilityArchive() {
        val compatibilityFiles =
            listOf("vendor_manifest.xml", "vendor_matrix.xml", "system_manifest.xml", "system_matrix.xml")
        val targetFile = ZipFile(theTargetFileName)
        val compZip = ZipArchiveOutputStream(File("${workDir}/compatability.zip"))
        compatibilityFiles.forEach {
            if (targetFile.getEntry("META/$it") != null) {
                val tmpFile = File.createTempFile("care.", ".entry")
                targetFile.dumpEntry("META/$it", tmpFile)
                compZip.pack(tmpFile, "META/$it")
                tmpFile.deleteOnExit()
            } else {
                log.warn("entry META/$it doesn't exist")
            }
        }
        compZip.finish()
        compZip.close()
    }

    fun writeMetadata(metadata: Properties, fileName: String) {
        val metaValueList = metadata.map { "${it.key}=${it.value}\n" }.sorted()
        val metaValue = metaValueList.reduce { acc, s -> "$acc$s" }
        log.info("writing metadata to file $fileName")
        File(fileName).writeText(metaValue)
    }

    //system/update_engine/scripts/brillo_update_payload generate
    fun writeABOTAPackageWithBrilloScript() {
        val targetFile = ZipFile(theTargetFileName)
        val stagingFile = "$workDir/staging.zip"
        val signedStagingFile = "$workDir/signedStaging.zip"
        val signedOTAFile = "$workDir/ota.zip"

        //prepare workspace
        if (File(workDir).exists()) {
            log.info("cleaning $workDir")
            File(workDir).deleteRecursively()
        }
        File(workDir).mkdirs()
        //properties check
        val infoDict = Common.loadInfoDict(theTargetFileName)
        if ("true" != infoDict.getProperty("ab_update")) {
            log.error("I only handle AB update target files")
            exitProcess(1)
        }
        if ("true" != infoDict.getProperty("avb_enable") && "true" != infoDict.getProperty("verity")) {
            throw IllegalArgumentException("only support verity/avb products")
        }
        val bFullTreble = infoDict.getBuildProp("ro.treble.enabled")
        if (bFullTreble == null || "true" != bFullTreble) {
            throw IllegalArgumentException("only treble devices are supported, but ro.treble.enabled=$bFullTreble")
        }
        val utc = infoDict.getBuildProp("ro.build.date.utc")!! //seconds since 1970
        val kindUTC = (Instant.ofEpochMilli(utc.toLong() * 1000L)
            .atZone(ZoneId.systemDefault()).toLocalDateTime()
            .plusYears(1)
            .atZone(ZoneId.systemDefault()).toInstant()
            .toEpochMilli() / 1000)
            .toString()

        //target.zip -> payload.bin
        val pg = PayloadGenerator()

        pg.generateMine(
            maxTs = kindUTC,
            inTargetFile = theTargetFileName,
            payload = "$workDir/payload.bin.mine",
            infoDict)

        //target.zip -> payload.bin
        pg.signMine(PayloadSigner(), options)

        //clear ota.zip: staging.zip
        ZipArchiveOutputStream(File(stagingFile)).use { zaos ->
            //payload.bin, payload prop
            zaos.pack(File(pg.signedPayload + ".mine"), "payload.bin", ZipMethod.STORED)
            zaos.pack(File(pg.propertiesFile + ".mine"), "payload_properties.txt", ZipMethod.STORED)

            //target.zip care map -> ota.zip care map
            val careMapList = listOfNotNull(
                targetFile.getEntry("META/care_map.pb"),
                targetFile.getEntry("META/care_map.txt"))
            careMapList.forEach {
                val tmpFile = File.createTempFile("care", "entry")
                targetFile.dumpEntry(it.name, File(tmpFile.path))
                zaos.pack(tmpFile, it.name, ZipMethod.STORED)
                tmpFile.deleteOnExit()
            }

            //compatibility zip
            addCompatibilityArchive()
            zaos.pack(File("${workDir}/compatability.zip"), "compatibility.zip", ZipMethod.STORED)

            zaos.finish()
        }

        //staging.zip -> propStrings
        val neededPropertyStrings = listOf(AbBrilloPropString(), StreamingBrilloPropString())
        val brilloPropStrings = computeAllPropertyStrings(stagingFile, neededPropertyStrings)
        rmMetaData(stagingFile)
        //add guessed metaDataName
        zipEdit(stagingFile,
            ZipHelper.ZipEntryRecipe(
                brilloPropStrings.toBrilloString().toByteArray(),
                BrilloPropString.metaDataName,
                ZipMethod.STORED))
        //preliminary signing
        AndroidHelper.signFile("/home/yu/work/ota/signapk.jar",
            stagingFile,
            signedStagingFile)

        //add new metaDataName
        val brilloPropStrings2 = finalizeAllPropertyFiles(signedStagingFile, neededPropertyStrings, brilloPropStrings)
        //delete metaDataName
        ZipHelper.zipDelete(File(signedStagingFile), BrilloPropString.metaDataName)
        zipEdit(signedStagingFile,
            ZipHelper.ZipEntryRecipe(
                brilloPropStrings2.toBrilloString().toByteArray(),
                BrilloPropString.metaDataName,
                ZipMethod.STORED))
        //sign again
        AndroidHelper.signFile("/home/yu/work/ota/signapk.jar",
            signedStagingFile,
            signedOTAFile)
        neededPropertyStrings.forEach {
            it.verify(signedOTAFile, brilloPropStrings2.getProperty(it.name))
        }
    }

    fun assembleOTA() {

    }
}
